fix(imap): confirm retired-UID tail via message count before flagging - #341
Open
odiechoo wants to merge 2 commits into
Open
fix(imap): confirm retired-UID tail via message count before flagging#341odiechoo wants to merge 2 commits into
odiechoo wants to merge 2 commits into
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The empty-enumeration guard added in 2.0.1 (#340) produces a persistent false positive on Gmail (and other servers that retire UIDs). When a folder's highest surviving UID sits below UIDNEXT - 1 — which happens routinely on Gmail whenever messages are relabeled, archived, or moved out of a folder — every sync logs a "Refusing to advance highest_uid" warning that never clears, and highest_uid stays pinned below UIDNEXT indefinitely.
Mailbox 'Work': UID FETCH <start>:* returned no UIDs but server UIDNEXT=<n> (<k> messages in range). Refusing to advance highest_uid to avoid skipping them; the next sync will retry.The range is genuinely empty (the UIDs were retired, not withheld), so there is nothing for the retry to recover — the warning recurs forever.
Root cause
empty_enumeration_anomaly (crates/core/src/imap/executor.rs) decides purely on UIDNEXT > start_uid. That assumes a UID gap between highest_uid and UIDNEXT implies undelivered messages. On Gmail this is false: UIDNEXT only ever increases, and relabeling / archiving / moving a message retires its UID in that folder without lowering UIDNEXT. So a folder whose last surviving message is UID 337 can sit at UIDNEXT=344 with 338–343 being retired (empty) slots. UID FETCH 338:* correctly returns nothing, but the guard reads that as "6 messages I'm refusing to skip" and re-flags every sync.
The ({} messages in range) figure is UIDNEXT - start_uid, i.e. a slot count, not a confirmed message count — for a retired tail the real count is 0.
Reproduction
Gmail account synced folder-by-folder (individual labels).
Remove the label from (or archive/move) the message(s) holding the top UIDs of a folder, so the highest surviving UID is below UIDNEXT - 1.
No new mail arrives in that folder.
Every sync logs the "Refusing to advance highest_uid" warning for that folder; it never clears (only real new mail, which pushes a non-empty {start}:*, would).
Impact
No data loss — the archive is append-only and complete for these folders; the tail is genuinely empty. But it produces permanent per-folder error spam and pins highest_uid, which is indistinguishable at a glance from a real truncation/loss anomaly. Users cannot tell benign retired tails from a genuine problem.
Fix
The guard cannot distinguish a truncated/throttled empty (real mail — must not skip) from a retired-UID tail (benign) from UIDNEXT alone. Confirm with a signal immune to SEARCH/FETCH truncation: the message count. The EXAMINE response already carries exists (server count), and the local stored count is equally authoritative.
When the enumeration is empty and the guard would fire:
If examined.exists <= local_stored_count → the folder holds no more mail than we already store → retired tail → advance highest_uid to UIDNEXT - 1.
Else → server genuinely has more than we hold → real gap → keep the current refuse-and-retry behavior.
This preserves the anti-truncation protection while eliminating the false positive. It's conservative on the oversized-skipped edge: if a folder has messages skipped for max_email_size_bytes, exists can exceed the local count and the guard simply falls through to the retry path — never skipping mail.
Changes: adds a cheap IndexManager::count_for_mailbox (Tantivy Count collector, no ID materialization), and a resolve_empty_enumeration helper called from both fetch_new_mail_range and fetch_new_mail_with_before.